package sideeffect

import (
	"context"
	"encoding/json"
	errors2 "errors"
	"fmt"
	"github.com/gohobby/deepcopy"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/kubeapi"
	"gitverse.ru/synapse/kubelatte/pkg/observability/logs"
	controllers "gitverse.ru/synapse/kubelatte/pkg/operator"
	util2 "gitverse.ru/synapse/kubelatte/pkg/operator"
	"gitverse.ru/synapse/kubelatte/pkg/operator/controllers/clientset"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	"gitverse.ru/synapse/kubelatte/pkg/util/templates"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes"
	"sigs.k8s.io/yaml"
	"strings"
	"sync"
	"time"
)

var annotSideEffConfigName = "kblt.sideEffect.resource/sideEffectConfigName"
var annotTriggerNamespace = "kblt.sideEffect.resource/triggerNamespace"
var annotTriggerName = "kblt.sideEffect.resource/triggerName"
var annotParentHash = "kblt.sideEffect.resource/parentHash"
var annotTag = "kblt.sideEffect.resource/tag"

var prefix = "kblt"
var emptyParentUpdateChecker = func(parentByte []byte, expectedTag int64) parentStatus {
	return publishedHashChanged
}

type Executor struct {
	kubeCli        *kubernetes.Clientset
	kbtlKubeClient clientset.V1Alpha1Interface
	mu             *sync.Mutex
}

func NewExecutor(kbltCli clientset.V1Alpha1Interface, kubeCli *kubernetes.Clientset) ExecutorI {
	return &Executor{
		kubeCli:        kubeCli,
		kbtlKubeClient: kbltCli,
		mu:             &sync.Mutex{},
	}
}

func (e *Executor) ApplySideEffect(ctx context.Context, info *SE) error {
	if util2.KbltSideEffect != "enabled" && util2.KbltSideEffect != "true" {
		return nil
	}

	if Mode(info.SeMode) == Recreation {
		e.applySideEffectRecreate(ctx, info)
		return nil
	}

	e.applySideEffectCreate(ctx, info)

	return nil
}

func (e *Executor) applySideEffectCreate(ctx context.Context, info *SE) {
	logs.Debugf("side effect starting; sideEffectConfig: %v; triggerName: %v; namespace: %v", info.Configs.TriggerRef.SideEffectConfig, info.Configs.TriggerRef.Name, info.Configs.Metadata.Namespace)

	parentByte, status, err := e.waitForParentPublication(ctx, controllers.TimeoutParentWaiting, info, emptyParentUpdateChecker)
	if err != nil {
		logs.Errorf("failed to check resource in: "+"ns %s; kind %s; resource %s; error %s", info.Configs.Metadata.Namespace, info.Configs.Kind, info.Configs.Metadata.Name, err.Error())
		return
	}
	if status == unknown {
		logs.Infof("parent resource does not exists during %s seconds, side effect will not be applied: resource %s, err %s", controllers.TimeoutParentWaiting, info.Configs.Kind+" "+info.Configs.Metadata.Name, err)
		return
	}

	parent, err := e.getResourceInfo(ctx, parentByte)
	if err != nil {
		return
	}

	e.createTIsByTemplateRefs(ctx, info, parent)
}

func (e *Executor) createTIsByTemplateRefs(ctx context.Context, info *SE, parent *resourceInfo) {
	trigName := info.Configs.TriggerRef.Name
	trigNs := info.Configs.TriggerRef.Namespace
	sideEffConfig := info.Configs.TriggerRef.SideEffectConfig

	templs := storage.Storage.GetTemplates()
	for _, ref := range info.Configs.TemplateRefs {

		templ, ok := templs[ref]
		if !ok {
			continue
		}

		renders := e.getRendersForTemplate(ctx, templ, parent.obj)
		for i := 0; i < len(renders); i++ {
			logs.Debugf("got render: %s", renders[i].Render)

			ti, err := e.renderTriggerInstance(ctx, renders[i].Render, &templ, trigName, trigNs, sideEffConfig, parent)
			if err != nil {
				return
			}

			e.publishTI(ctx, ti, info)
		}
	}
}

func (e *Executor) waitForParentPublication(ctx context.Context, timeout time.Duration, info *SE,
	checkParentUpdate func(parentByte []byte, expectedTag int64) parentStatus) ([]byte, parentStatus, error) {
	var parentByte []byte
	var err error
	var status parentStatus = unknown
	var statuses = map[parentStatus]string{unknown: "unknown", notYetUpdated: "notYetUpdated", publishedHashNotChanged: "publishedHashNotChanged", publishedHashChanged: "publishedHashChanged", obtained: "obtained"}

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	for {
		select {
		case <-ctx.Done():
			if err != nil {
				return parentByte, status, err
			}
			if parentByte == nil {
				return nil, status, errors2.New("no parent info")
			}
			return parentByte, status, err
		default:
			parentByte, err = e.getResourceFromKubeApi(
				ctx, info.Configs.Kind, info.Configs.ApiVersion, info.Configs.Metadata.Name, info.Configs.Metadata.Namespace)
			if err == nil && parentByte != nil {
				status = checkParentUpdate(parentByte, info.Configs.ParentTag)
				if status == unknown || status == notYetUpdated {
					logs.Debug("got status for parent publication: ", statuses[status])
					continue
				}
				if status == obtained ||
					status == publishedHashChanged ||
					status == publishedHashNotChanged {
					return parentByte, status, nil
				}
			}

			time.Sleep(200 * time.Millisecond)
		}
	}
}

func (e *Executor) getResourceFromKubeApi(ctx context.Context, kind, apiVersion, name, namespace string) ([]byte, error) {
	resourceMeta := kubeapi.GetResourceMeta(e.kubeCli, kind, apiVersion)
	if resourceMeta == nil {
		return nil, fmt.Errorf("failed to get meta for resource : ns %s; apiVersion %s; kind %s ", namespace, apiVersion, kind)
	}

	res, err := kubeapi.GetResource(ctx, resourceMeta, name, namespace)
	return res, err
}

func (e *Executor) publishTI(ctx context.Context, ti *v1alpha1.TriggerInstance, info *SE) { //nolint:gocritic
	_, err := e.kbtlKubeClient.TriggerInstance().Get(ctx, info.Configs.Metadata.Namespace, ti.Name, metav1.GetOptions{})
	if err != nil {
		if !errors.IsNotFound(err) {
			logs.Errorf("TriggerInstance.Get error: %v", err)
			return
		}
	} else {
		logs.Debugf("trigger instance %v already exists", ti)
		return
	}

	_, err = e.kbtlKubeClient.TriggerInstance().Create(ctx, ti.Namespace, ti)
	if err != nil {
		logs.Errorf("creation trigger instance error %v", err)
		return
	}
}

type resourceInfo struct {
	kind       string
	apiVersion string
	namespace  string
	name       string
	uid        types.UID
	objYaml    []byte
	obj        map[string]interface{}
}

func (e *Executor) getResourceInfo(ctx context.Context, obj []byte) (*resourceInfo, error) {
	var info resourceInfo

	var d interface{}
	err := json.Unmarshal(obj, &d)
	if err != nil {
		logs.Errorf("json unmarshalling error: %s, obj %s", err.Error(), string(obj))
		return nil, err
	}

	data := d.(map[string]interface{})

	info.obj = data
	info.kind = data["kind"].(string)
	info.apiVersion = data["apiVersion"].(string)

	metadata := data["metadata"].(map[string]interface{})
	metadata["managedFields"] = nil

	info.name = metadata["name"].(string)
	info.namespace = metadata["namespace"].(string)
	info.uid = types.UID(metadata["uid"].(string))

	ya, err := yaml.Marshal(d)
	if err != nil {
		logs.Errorf("yaml marshalling error: %s, obj %s", err, obj)
		return nil, err
	}
	info.objYaml = ya

	return &info, nil
}

func (e *Executor) renderTriggerInstance(ctx context.Context, render string, template *v1alpha1.Template,
	trigName, trigNs, seConfig string, parent *resourceInfo) (*v1alpha1.TriggerInstance, error) {

	var ti *v1alpha1.TriggerInstance

	err := yaml.Unmarshal([]byte(render), &ti)
	if err != nil {
		logs.Errorf("yaml unmarshal error: %v", err)
		return nil, err
	}

	hashControlFields, err := e.getObjControlFieldsHash(ctx, parent.obj)
	if err != nil {
		return nil, err
	}
	if ti.Annotations == nil {
		ti.Annotations = make(map[string]string)
	}
	ti.Annotations[annotParentHash] = hashControlFields
	ti.Annotations[annotSideEffConfigName] = seConfig
	ti.Annotations[annotTriggerName] = trigName
	ti.Annotations[annotTriggerNamespace] = trigNs

	ti.Name = e.getNameFromTemplate(ctx, template)
	if ti.Name == "" {
		ti.Name = parent.name
	}
	ti.Name += "-" + controllers.GetShortHash(controllers.GetRandomPart()+template.Name+template.Spec.Kind)

	ti.Namespace = parent.namespace
	ti.Spec.Data = string(parent.objYaml)

	if controllers.OwnerRefSideEffect == "enabled" ||
		controllers.OwnerRefSideEffect == "true" ||
		controllers.OwnerRefSideEffect == "" {
		ti.OwnerReferences = append(ti.OwnerReferences, metav1.OwnerReference{
			APIVersion: parent.apiVersion,
			Kind:       parent.kind,
			Name:       parent.name,
			UID:        parent.uid,
		})
	}

	return ti, nil
}

func (e *Executor) getNameFromTemplate(ctx context.Context, t *v1alpha1.Template) string { //nolint:gocritic
	var d interface{}
	err := yaml.Unmarshal([]byte(t.Spec.Data), &d)
	if err != nil {
		logs.Errorf("yaml unmarshalling error template.spec.data in template %s, err: %s", t.Name, err)
		return ""
	}
	data, ok := d.(map[string]interface{})
	if !ok {
		return ""
	}

	m, ok := data["metadata"]
	if !ok {
		return ""
	}

	metadata, ok := m.(map[string]interface{})
	if !ok {
		return ""
	}
	n, ok := metadata["name"]
	if !ok {
		return ""
	}
	name, ok := n.(string)
	if !ok || name == "" {
		return ""
	}

	return name
}

func (e *Executor) getRendersForTemplate(ctx context.Context, t v1alpha1.Template, obj map[string]interface{}) []util.RenderItem { //nolint:gocritic
	var renders []util.RenderItem
	engine := templates.LoadTemplate(context.Background(), t)

	for _, engineTemplate := range engine.Templates() {
		var tempBuffer strings.Builder
		if len(engineTemplate.Templates()) == 0 {
			continue
		}

		obj := deepcopy.DeepCopy(obj).(map[string]interface{})
		err := engineTemplate.Execute(&tempBuffer, obj)
		if err != nil {
			logs.Errorf("executing template error: %s", err)
			continue
		}
		renders = append(renders, util.RenderItem{
			Template: t,
			Render:   tempBuffer.String(),
			Action:   "SIDEEFFECT",
		})
	}

	return renders
}
